--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit e999ff99b604dd4b4e0eba3c0fd6d9fd6d9811fd
Parents : ecbb470
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-18T07:19:41-05:00
feat: improve network visualiser performance with updated physics parameters and velocity handling
Changes
8 files changed, 115 insertions(+), 30 deletions(-)
Diff
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 0dad8798..a28e612d 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
index c232e1ce..ffd325e2 100644
--- a/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
+++ b/meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue
@@ -1039,10 +1039,10 @@ export default {
barnesHut: {
// Match upstream gravity, as avoidOverlap is O(n) per tick
// and is the main reason we felt slower than MeshChat.
- gravitationalConstant: -5000,
- springConstant: 0.04,
+ gravitationalConstant: -3500,
+ springConstant: 0.03,
springLength: 200,
- damping: 0.35,
+ damping: 0.55,
avoidOverlap: 0,
theta: 0.6,
},
@@ -1051,9 +1051,9 @@ export default {
iterations: 80,
updateInterval: 50,
},
- maxVelocity: 50,
- minVelocity: 0.75,
- timestep: 0.5,
+ maxVelocity: 22,
+ minVelocity: 1.2,
+ timestep: 0.35,
},
nodes: {
borderWidth: 2,
diff --git a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
index 21de699c..6dfadaea 100644
--- a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
+++ b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
@@ -356,7 +356,7 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
rafId = requestAnimationFrame(frame);
const live = typeof hooks.getLiveLayout === "function" ? hooks.getLiveLayout() : false;
if (live && pointerMode !== "drag") {
- callScene("meshchatxVisualiserSceneTick", 2);
+ callScene("meshchatxVisualiserSceneTick", 1);
dirty = true;
}
if (!dirty && !live) return;
diff --git a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
index ac89830a..da43ac84 100644
--- a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
+++ b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
@@ -1,6 +1,6 @@
{
"version": "1.2.0",
- "wasm": "sha384-qIr9tHzeIj1WPDNVA/HjxTBvvKMXeIFMWdq3S6ZGXljC/VWgs1IQ9mCbjyVw7NBD",
+ "wasm": "sha384-c+lFXFxN/i2AcZZSeL7Yg72zty8ZP3YUVcirt28JfJ/+HUPNL1LyDxZg8GgKspiy",
"wasmExec": "sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce",
"wasmExecSource": "/usr/lib/go/lib/wasm/wasm_exec.js"
}
diff --git a/visualiser-wasm/internal/layout/force.go b/visualiser-wasm/internal/layout/force.go
index 5a807b2d..e1000bc9 100644
--- a/visualiser-wasm/internal/layout/force.go
+++ b/visualiser-wasm/internal/layout/force.go
@@ -14,6 +14,8 @@ type Node struct {
ID string `json:"id"`
X float64 `json:"x"`
Y float64 `json:"y"`
+ Vx float64 `json:"vx"`
+ Vy float64 `json:"vy"`
Mass float64 `json:"mass"`
Fixed bool `json:"fixed"`
}
@@ -75,23 +77,24 @@ func Settle(req Request) Result {
gravity := req.Gravity
if gravity == 0 {
- gravity = 0.012
+ gravity = 0.01
}
repulsion := req.Repulsion
if repulsion == 0 {
- repulsion = 1800
+ repulsion = 1200
}
springK := req.SpringK
if springK == 0 {
- springK = 0.045
+ springK = 0.032
}
damping := req.Damping
if damping == 0 {
- damping = 0.82
+ // Multiply kept velocity each step. Lower = more friction, less twitch.
+ damping = 0.68
}
maxSpeed := req.MaxSpeed
if maxSpeed == 0 {
- maxSpeed = 40
+ maxSpeed = 18
}
bodies := make([]body, n)
@@ -106,6 +109,8 @@ func Settle(req Request) Result {
id: nd.ID,
x: nd.X,
y: nd.Y,
+ vx: nd.Vx,
+ vy: nd.Vy,
mass: mass,
fixed: nd.Fixed || nd.ID == "me",
}
@@ -228,6 +233,14 @@ func Settle(req Request) Result {
for i := range bodies {
out.Positions[bodies[i].id] = XY{X: bodies[i].x, Y: bodies[i].y}
+ // Write velocities back onto the request nodes when ids match so
+ // live layout can carry momentum across Tick frames.
+ if idx, ok := index[bodies[i].id]; ok {
+ req.Nodes[idx].X = bodies[i].x
+ req.Nodes[idx].Y = bodies[i].y
+ req.Nodes[idx].Vx = bodies[i].vx
+ req.Nodes[idx].Vy = bodies[i].vy
+ }
}
out.Iterations = iters
return out
diff --git a/visualiser-wasm/internal/layout/force_test.go b/visualiser-wasm/internal/layout/force_test.go
index 98d11faa..e404b324 100644
--- a/visualiser-wasm/internal/layout/force_test.go
+++ b/visualiser-wasm/internal/layout/force_test.go
@@ -33,10 +33,24 @@ func TestSettleMovesUnfixedNodes(t *testing.T) {
}
}
-func TestSettleEmpty(t *testing.T) {
- res := layout.Settle(layout.Request{})
- if len(res.Positions) != 0 {
- t.Fatal("expected empty")
+func TestSettlePreservesAndUpdatesVelocity(t *testing.T) {
+ nodes := []layout.Node{
+ {ID: "me", X: 0, Y: 0, Mass: 4, Fixed: true},
+ {ID: "a", X: 20, Y: 0, Vx: 5, Vy: 0, Mass: 1},
+ }
+ res := layout.Settle(layout.Request{
+ Nodes: nodes,
+ Edges: []layout.Edge{{From: "me", To: "a", Length: 180}},
+ Iterations: 3,
+ Damping: 0.5,
+ MaxSpeed: 20,
+ })
+ if len(res.Positions) != 2 {
+ t.Fatalf("positions %d", len(res.Positions))
+ }
+ // Request nodes are updated in place so live Tick can carry momentum.
+ if nodes[1].Vx == 5 && nodes[1].X == 20 {
+ t.Fatalf("expected velocity/position to integrate, got vx=%v x=%v", nodes[1].Vx, nodes[1].X)
}
}
diff --git a/visualiser-wasm/internal/scene/scene.go b/visualiser-wasm/internal/scene/scene.go
index 76a6f664..cb3976d6 100644
--- a/visualiser-wasm/internal/scene/scene.go
+++ b/visualiser-wasm/internal/scene/scene.go
@@ -74,6 +74,8 @@ type Scene struct {
nodes []Node
edges []Edge
index map[string]int
+ vx []float64
+ vy []float64
width float64
height float64
camX float64
@@ -130,6 +132,8 @@ func (s *Scene) Set(req SetRequest) {
s.camY = req.CamY
}
s.dragIdx = -1
+ s.vx = make([]float64, len(s.nodes))
+ s.vy = make([]float64, len(s.nodes))
}
func defaultSize(kind int) float64 {
@@ -227,10 +231,14 @@ func (s *Scene) Tick(steps int) {
return
}
if steps <= 0 {
- steps = 2
+ steps = 1
}
- if steps > 8 {
- steps = 8
+ if steps > 3 {
+ steps = 3
+ }
+ if len(s.vx) != len(s.nodes) || len(s.vy) != len(s.nodes) {
+ s.vx = make([]float64, len(s.nodes))
+ s.vy = make([]float64, len(s.nodes))
}
layoutNodes := make([]layout.Node, len(s.nodes))
for i := range s.nodes {
@@ -243,6 +251,8 @@ func (s *Scene) Tick(steps int) {
ID: n.ID,
X: n.X,
Y: n.Y,
+ Vx: s.vx[i],
+ Vy: s.vy[i],
Mass: n.Mass,
Fixed: fixed,
}
@@ -250,9 +260,9 @@ func (s *Scene) Tick(steps int) {
layoutEdges := make([]layout.Edge, 0, len(s.edges))
for i := range s.edges {
e := &s.edges[i]
- length := 180.0
+ length := 200.0
if e.Width >= 2.5 {
- length = 150
+ length = 170
}
layoutEdges = append(layoutEdges, layout.Edge{
From: e.From,
@@ -260,18 +270,31 @@ func (s *Scene) Tick(steps int) {
Length: length,
})
}
- res := layout.Settle(layout.Request{
+ // Softer than one-shot Settle defaults so live layout does not twitch.
+ _ = layout.Settle(layout.Request{
Nodes: layoutNodes,
Edges: layoutEdges,
Iterations: steps,
+ Gravity: 0.004,
+ Repulsion: 550,
+ SpringK: 0.018,
+ Damping: 0.52,
+ MaxSpeed: 6,
})
+ const restSpeed = 0.12
for i := range s.nodes {
- if p, ok := res.Positions[s.nodes[i].ID]; ok {
- if s.dragIdx == i {
- continue
- }
- s.nodes[i].X = p.X
- s.nodes[i].Y = p.Y
+ if s.dragIdx == i {
+ s.vx[i] = 0
+ s.vy[i] = 0
+ continue
+ }
+ s.nodes[i].X = layoutNodes[i].X
+ s.nodes[i].Y = layoutNodes[i].Y
+ s.vx[i] = layoutNodes[i].Vx
+ s.vy[i] = layoutNodes[i].Vy
+ if math.Hypot(s.vx[i], s.vy[i]) < restSpeed {
+ s.vx[i] = 0
+ s.vy[i] = 0
}
}
}
diff --git a/visualiser-wasm/internal/scene/scene_test.go b/visualiser-wasm/internal/scene/scene_test.go
index c33e03a9..baf1f2b2 100644
--- a/visualiser-wasm/internal/scene/scene_test.go
+++ b/visualiser-wasm/internal/scene/scene_test.go
@@ -2,7 +2,10 @@
package scene
-import "testing"
+import (
+ "math"
+ "testing"
+)
func TestSetPackAndPick(t *testing.T) {
s := New()
@@ -99,3 +102,35 @@ func TestTickMovesUnfixed(t *testing.T) {
_ = before
}
}
+
+func TestTickPersistsVelocityAndSettles(t *testing.T) {
+ s := New()
+ s.Set(SetRequest{
+ Nodes: []Node{
+ {ID: "me", X: 0, Y: 0, Kind: KindMe, Fixed: true, Mass: 4},
+ // Start near spring rest length so live ticks should calm quickly.
+ {ID: "a", X: 170, Y: 0, Kind: KindPeer, Mass: 1},
+ {ID: "b", X: -170, Y: 0, Kind: KindPeer, Mass: 1},
+ },
+ Edges: []Edge{
+ {From: "me", To: "a", Width: 3},
+ {From: "me", To: "b", Width: 3},
+ },
+ })
+ for i := 0; i < 30; i++ {
+ s.Tick(1)
+ }
+ x1 := s.nodes[1].X
+ y1 := s.nodes[1].Y
+ for i := 0; i < 30; i++ {
+ s.Tick(1)
+ }
+ step := math.Hypot(s.nodes[1].X-x1, s.nodes[1].Y-y1)
+ // Near-equilibrium live layout must not keep thrashing.
+ if step > 12 {
+ t.Fatalf("live layout still twitching after settle: moved %v", step)
+ }
+ if math.Hypot(s.vx[1], s.vy[1]) > 1.2 {
+ t.Fatalf("velocity should damp toward rest, got %v %v", s.vx[1], s.vy[1])
+ }
+}
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────